using System;
using System.Collections;
using System.Xml;
using System.IO;
using System.Text;
using System.Reflection;
using System.Text.RegularExpressions;
using Mono.CompilerServices.SymbolWriter;
using Mono.Cecil;
using Mono.Cecil.Metadata;

namespace MonoCov
{

public delegate void CoverageProgress (string item, double percent);

public class CoverageModel : CoverageItem {

	private Hashtable namespaces;
	private Hashtable classes;
	private Hashtable sources;

	private Hashtable loadedAssemblies;
	private Hashtable symbolFiles;

	public event CoverageProgress Progress;

	/**
	 * List of filters, which are strings
	 */
	private ArrayList filters;

	public CoverageModel ()
	{
		namespaces = new Hashtable ();
		classes = new Hashtable ();
		sources = new Hashtable ();
		filters = new ArrayList ();
		Progress += delegate {}; // better than having to check every time...
	}

	public Hashtable Classes {
		get {
			return classes;
		}
	}

	public Hashtable Namespaces {
		get {
			return namespaces;
		}
	}

	public void AddFilter (String pattern) {
		filters.Add (pattern);
	}

	private bool IsFiltered (string name)
	{

		// Check positive filters first
		bool hasPositive = false;
		bool found = false;
		foreach (String pattern in filters) {
			if (pattern [0] == '+') {
				string p = pattern.Substring (1);
				if (name.IndexOf (p) != -1) {
					//Console.WriteLine ("FILTERED: " + pattern + " -> " + name);
					found = true;
				}
				hasPositive = true;
			}
		}
		if (hasPositive && !found)
			return true;

		foreach (String pattern in filters) {
			if (pattern [0] == '-') {
				string p = pattern.Substring (1);
				if (name.IndexOf (p) != -1) {
					//Console.WriteLine ("FILTERED: " + pattern + " -> " + name);
					return true;
				}
			}
		}
		return false;
	}

	private void LoadAssemblies (XmlDocument dom)
	{
		foreach (XmlNode n in dom.GetElementsByTagName ("assembly")) {
			string assemblyName = n.Attributes ["name"].Value;
			string guid = n.Attributes ["guid"].Value;
			string filename = n.Attributes ["filename"].Value;
			MonoSymbolFile symbolFile;

#if USE_REFLECTION
			Assembly assembly = Assembly.Load (assemblyName);

			MethodInfo getguid = typeof (Module).GetMethod (
					"Mono_GetGuid", BindingFlags.Instance|BindingFlags.Public|BindingFlags.NonPublic,
					null, CallingConventions.Any, new Type [0], null);

			if (getguid != null) {
				Guid assembly_guid = (Guid)getguid.Invoke (assembly.GetLoadedModules ()[0], new object [0]);
				Console.WriteLine (assembly_guid);
				if (assembly_guid != new Guid (guid)) {
					Console.WriteLine ("WARNING: Loaded version of assembly " + assembly + " is different from the version used to collect coverage data.");
				}
			} else {
				Console.WriteLine ("WARNING: Can't verify the guid of " + assembly);
			}

			loadedAssemblies [assemblyName] = assembly;

			Console.Write ("Reading symbols for " + assembly + " ...");
			symbolFile = MonoSymbolFile.ReadSymbolFile (assembly);
			if (symbolFile == null)
				Console.WriteLine (" (No symbols found)");
			else {
				symbolFiles [assembly] = symbolFile;
				Console.WriteLine (" (" + symbolFile.SourceCount + " files, " + symbolFile.MethodCount + " methods)");
			}
#else
			AssemblyDefinition assembly = AssemblyFactory.GetAssembly (filename);
			ModuleDefinition module = assembly.MainModule;
			if (module.Mvid != new Guid (guid)) {
				Console.WriteLine ("WARNING: Loaded version of assembly " + assembly + " is different from the version used to collect coverage data.");
			}
			loadedAssemblies [assemblyName] = assembly;

			Console.Write ("Reading symbols for " + assemblyName + " ...");
			symbolFile = MonoSymbolFile.ReadSymbolFile (filename + ".mdb");
			if (symbolFile == null)
				Console.WriteLine (" (No symbols found)");
			else {
				symbolFiles [assembly] = symbolFile;
				Console.WriteLine (" (" + symbolFile.SourceCount + " files, " + symbolFile.MethodCount + " methods)");
			}
#endif
		}
	}		

	private void LoadFilters (XmlDocument dom)
	{
		foreach (XmlNode n in dom.GetElementsByTagName ("filter")) {
			AddFilter (n.Attributes ["pattern"].Value);
		}
	}

#if USE_REFLECTION
	static Type LoadType (Assembly assembly, string name) {
		Type type = assembly.GetType (name);
		if (type != null)
			return type;
		int last_dot = name.LastIndexOf ('.');
		// covert names from IL to reflection naming
		// needed to deal with nested types
		while (last_dot >= 0) {
			StringBuilder sb = new StringBuilder (name);
			sb [last_dot] = '/';
			name = sb.ToString ();
			type = assembly.GetType (name);
			if (type != null)
				return type;
			last_dot = name.LastIndexOf ('.');
		}
		return null;
	}
#else
	static TypeDefinition LoadType (AssemblyDefinition assembly, string name) {
		TypeDefinition type = assembly.MainModule.Types [name];
		if (type != null)
			return type;
		int last_dot = name.LastIndexOf ('.');
		// covert names from IL to reflection naming
		// needed to deal with nested types
		while (last_dot >= 0) {
			StringBuilder sb = new StringBuilder (name);
			sb [last_dot] = '/';
			name = sb.ToString ();
			type = assembly.MainModule.Types [name];
			if (type != null)
				return type;
			last_dot = name.LastIndexOf ('.');
		}
		return null;
	}
#endif

	public void ReadFromFile (string fileName)
	{
		namespaces = new Hashtable ();
		classes = new Hashtable ();

		long begin = DateTime.Now.Ticks / 10000;
		long msec = DateTime.Now.Ticks / 10000;
		long msec2;

		loadedAssemblies = new Hashtable ();
		symbolFiles = new Hashtable ();

		XmlDocument dom = new XmlDocument ();
		Progress ("XML reading", 0);
		Console.Write ("Loading " + fileName + "...");
		dom.Load (new XmlTextReader (new FileStream (fileName, FileMode.Open)));
		Console.WriteLine (" Done.");

		msec2 = DateTime.Now.Ticks / 10000;
		Console.WriteLine ("XML Reading: " + (msec2 - msec) + " msec");
		msec = msec2;

		Progress ("Load assemblies", 0.2);
		LoadAssemblies (dom);

		LoadFilters (dom);

		msec2 = DateTime.Now.Ticks / 10000;
		Console.WriteLine ("Load assemblies: " + (msec2 - msec) + " msec");
		msec = msec2;

		Progress ("Load methods", 0.4);
		foreach (XmlNode n in dom.GetElementsByTagName ("method")) {
			string assemblyName = n.Attributes ["assembly"].Value;
			string className = n.Attributes ["class"].Value;
			string methodName = n.Attributes ["name"].Value;
			string token = n.Attributes ["token"].Value;
			string cov_info = n.FirstChild.Value;
			int itok = int.Parse (token);
			
#if USE_REFLECTION
			Assembly assembly = (Assembly)loadedAssemblies [assemblyName];
			MonoSymbolFile symbolFile = (MonoSymbolFile)symbolFiles [assembly];

			if (symbolFile == null)
				continue;

			Type t = LoadType (assembly, className);
			if (t == null) {
				Console.WriteLine ("ERROR: Unable to resolve type " + className + " in " + assembly);
				continue;
			}

			ClassCoverageItem klass = ProcessClass (t);

			MethodEntry entry = symbolFile.GetMethodByToken (Int32.Parse (token));

			Module[] modules = assembly.GetModules();

			if (modules.Length > 1)
				Console.WriteLine("WARNING: Assembly had more than one module. Using the first.");

			Module module = modules[0];

			MethodBase monoMethod = module.ResolveMethod(Int32.Parse(token));

			ProcessMethod (monoMethod, entry, klass, methodName, cov_info);
#else
			if ((TokenType)(itok & 0xff000000) != TokenType.Method)
				continue;
			AssemblyDefinition assembly = (AssemblyDefinition)loadedAssemblies [assemblyName];
			MonoSymbolFile symbolFile = (MonoSymbolFile)symbolFiles [assembly];

			if (symbolFile == null)
				continue;

			TypeDefinition t = LoadType (assembly, className);
			if (t == null) {
				Console.WriteLine ("ERROR: Unable to resolve type " + className + " in " + assembly);
				continue;
			}

			ClassCoverageItem klass = ProcessClass (t);

			MethodEntry entry = symbolFile.GetMethodByToken (itok);

			MethodDefinition monoMethod = assembly.MainModule.LookupByToken (
				new MetadataToken ((TokenType)(itok & 0xff000000), (uint)(itok & 0xffffff)))
				as MethodDefinition;
			//Console.WriteLine (monoMethod);
			ProcessMethod (monoMethod, entry, klass, methodName, cov_info);
#endif
		}

		msec2 = DateTime.Now.Ticks / 10000;
		Console.WriteLine ("Process methods: " + (msec2 - msec) + " msec");
		msec = msec2;

		// Add info for klasses for which we have no coverage

#if USE_REFLECTION
		foreach (Assembly assembly in loadedAssemblies.Values) {
			foreach (Type t in assembly.GetTypes ()) {
				ProcessClass (t);
			}
		}

		// Add info for methods for which we have no coverage

		foreach (ClassCoverageItem klass in classes.Values) {
			foreach (MethodInfo mb in klass.type.GetMethods (BindingFlags.NonPublic|BindingFlags.Public|BindingFlags.Static|BindingFlags.Instance | BindingFlags.DeclaredOnly)) {
				MonoSymbolFile symbolFile = (MonoSymbolFile)symbolFiles [klass.type.Assembly];
				if (symbolFile == null)
					continue;

				if (! klass.methodsByMethod.ContainsKey (mb)) {
					MethodEntry entry = symbolFile.GetMethod (mb);
					ProcessMethod (mb, entry, klass, mb.Name, null);
				}
			}
		}
#else
		Progress ("Not covered classes", 0.6);
		foreach (AssemblyDefinition assembly in loadedAssemblies.Values) {
			foreach (TypeDefinition t in assembly.MainModule.Types) {
				ProcessClass (t);
			}
		}

		Progress ("Not covered methods", 0.7);
		// Add info for methods for which we have no coverage
		foreach (ClassCoverageItem klass in classes.Values) {
			foreach (MethodDefinition mb in klass.type.Methods) {
				MonoSymbolFile symbolFile = (MonoSymbolFile)symbolFiles [klass.type.Module.Assembly];
				if (symbolFile == null)
					continue;

				if (! klass.methodsByMethod.ContainsKey (mb)) {
					MethodEntry entry = symbolFile.GetMethodByToken ((int)mb.MetadataToken.ToUInt());
					string name = GetMethodNameWithParameters( mb );
					ProcessMethod (mb, entry, klass, name, null);
				}
			}
		}
#endif

		msec2 = DateTime.Now.Ticks / 10000;
		Console.WriteLine ("Additional classes: " + (msec2 - msec) + " msec");
		msec = msec2;

		Progress ("Compute coverage", 0.9);
		// Compute coverage for all items

		computeCoverage (true);

		msec2 = DateTime.Now.Ticks / 10000;
		Console.WriteLine ("Compute coverage: " + (msec2 - msec) + " msec");
		msec = msec2;

		Console.WriteLine ("All: " + (msec2 - begin) + " msec");
		Progress ("Done loading", 0.9);

		// Free memory
		symbolFiles = null;
	}

	private string GetMethodNameWithParameters(MethodDefinition method)
   {
		if (method.Parameters.Count == 0)
			return method.Name + " ()";
		// Monocov output for overloaded methods look like:
		// MethodName (string,byte[],System.Data.DataTable)
		// MethodName (string,int)
		string[] paramList = new string[method.Parameters.Count];
		int i = 0;
		foreach (ParameterDefinition param in method.Parameters)
		{
			paramList[i] = GetParameterName(param);
			i++;
		}
		return method.Name + " (" + string.Join(",", paramList ) +")";
	}

	// format uses full name for non-value types
   // value types, strings, objects use short name.
	// for types with alias, the alias is used: int32 -> int
	private string GetParameterName(ParameterDefinition param)
	{
		if (param.ParameterType.IsValueType || 
			 param.ParameterType.FullName.Equals("System.String") ||
			 param.ParameterType.FullName.Equals("System.Object") )
		{
			string name = param.ParameterType.Name.ToLower();
			//if (name.EndsWith("16") ||
			//	 name.EndsWith("32") ||
			//	 name.EndsWith("64"))
   		if (name.IndexOf("16") > -1 ||
             name.IndexOf("32") > -1 ||
             name.IndexOf("64") > -1 )
			{
			//	name = name.Substring(0, name.Length - 2);
				name = name.Replace("16", "");
				name = name.Replace("32", "");
				name = name.Replace("64", "");
			}
			return name;
		}
		else
			return param.ParameterType.FullName;
	}


	//
	// Computes the coverage of METHOD
	//

	private char[] digits = "0123456789".ToCharArray ();
	private char[] ws = "\t\n ".ToCharArray ();

	private int parsePositiveInteger (string s, int pos) {
		int n = 0;

		while (s [pos] >= '0' && s [pos] <= '9'){
			n = n * 10 + (s [pos] - '0');
			pos ++;
		}
		return n;
	}

	private void computeMethodCoverage (MethodCoverageItem method, LineNumberEntry[] lines, string cov_info)
	{

		ClassCoverageItem klass = method.Class;
		SourceFileCoverageData source = klass.sourceFile;

		source.AddMethod (method);

		int nlines = method.endLine - method.startLine + 1;

		int[] coverage = new int [nlines];

		if (cov_info == null) {
			for (int i = 0; i < nlines; ++i)
				coverage [i] = 0;
		}
		else {
			for (int i = 0; i < nlines; ++i)
				coverage [i] = -1;

			// Hand crafted parsing code since this is performance critical
			int pos = 0;
			int prev_offset = 0;
			while (pos < cov_info.Length) {
				int pos2 = cov_info.IndexOfAny (digits, pos);
				if (pos2 == -1)
					break;
				pos = cov_info.IndexOfAny (ws, pos2);
				if (pos == -1)
					break;

				int offset = parsePositiveInteger (cov_info, pos2);

				pos2 = cov_info.IndexOfAny (digits, pos);
				if (pos2 == -1)
					break;
				pos = cov_info.IndexOfAny (ws, pos2);

				int count = parsePositiveInteger (cov_info, pos2);

				offset += prev_offset;
				prev_offset = offset;

				int line1 = 0;
				int line2 = 0;

				bool found = GetSourceRangeFor (offset, method, lines, ref line1, ref line2);

				/*
				  if (found && (entry.Name.IndexOf ("Find") != -1)) {
				  Console.WriteLine ("OFFSET: " + offset + " " + line1 + ":" + line2);
				  }
				*/

				if (found) {
					for (int i = line1; i < line2 + 1; ++i)
						if ((i >= method.startLine) && (i <= method.endLine))
							if (coverage [i - method.startLine] < count)
								coverage [i - method.startLine] = count;
				}
			}
		}

		int hit = 0;
		int missed = 0;

		for (int i = 0; i < nlines; ++i) {
			int count = coverage [i];
			if (count > 0)
				hit ++;
			else if (count == 0)
				missed ++;
		}

		method.setCoverage (hit, missed);

		method.lineCoverage = coverage;
	}

	//
	// Return a range of source lines which have something to do with OFFSET.
	//
	private bool GetSourceRangeFor (int offset, MethodCoverageItem method,
					   LineNumberEntry[] lines,
					   ref int startLine, ref int endLine)
	{
		for (int i = 0; i < lines.Length; ++i) {
			if (offset >= lines [i].Offset)
				if (i == lines.Length - 1) {
					startLine = lines [i].Row;
					endLine = lines [i].Row;
					return true;
				}
				else if (offset < lines [i + 1].Offset) {
					startLine = lines [i].Row;
					endLine = lines [i + 1].Row - 1;
					return true;
				}
		}

		if (offset <= lines [0].Offset) {
			return false;
		}
		else {
			for (int i = 0; i < lines.Length; ++i)
				Console.WriteLine (lines [i]);
			throw new Exception ("Unable to determine source range for offset " + offset + " in " + method.name);
		}
	}

#if USE_REFLECTION
	private ClassCoverageItem ProcessClass (Type t)
#else
	private ClassCoverageItem ProcessClass (TypeDefinition t)
#endif
	{
		string className = t.FullName;
		int nsindex = className.LastIndexOf (".");
		string namespace2;
		string scopedName;
		if (nsindex == -1) {
			namespace2 = "<GLOBAL>";
			scopedName = className;
		} else if (nsindex == 0) {
			namespace2 = "<GLOBAL>";
			scopedName = className.Substring (1);
		}
		else {
			namespace2 = className.Substring (0, nsindex);
			scopedName = className.Substring (nsindex + 1);
		}

		// Create namespaces
		NamespaceCoverageItem ns = (NamespaceCoverageItem)namespaces [namespace2];
		if (ns == null) {
			string nsPrefix = "";
			foreach (String nsPart in namespace2.Split ('.')) {
				if (nsPrefix == "")
					nsPrefix = nsPart;
				else
					nsPrefix = nsPrefix + "." + nsPart;
				NamespaceCoverageItem ns2 = (NamespaceCoverageItem)namespaces [nsPrefix];
				if (ns2 == null) {
					if (ns == null)
						ns2 = new NamespaceCoverageItem (this, nsPrefix);
					else
						ns2 = new NamespaceCoverageItem (ns, nsPrefix);
					namespaces [nsPrefix] = ns2;
				}
				ns = ns2;
			}					
		}

		ClassCoverageItem klass = (ClassCoverageItem)classes [className];
		if (klass == null) {
			klass = new ClassCoverageItem (ns);
			klass.name_space = namespace2;
			klass.name = scopedName;
			klass.type = t;
			klass.parent = ns;

#if USE_REFLECTION
			klass.filtered = IsFiltered ("[" + t.Assembly + "]" + className);
#else
			klass.filtered = IsFiltered ("[" + t.Module.Name + "]" + className);
#endif
			classes [className] = klass;
		}

		return klass;
	}

#if USE_REFLECTION
	private void ProcessMethod (MethodBase monoMethod, MethodEntry entry, ClassCoverageItem klass, string methodName, string cov_info)
#else
	private void ProcessMethod (MethodDefinition monoMethod, MethodEntry entry, ClassCoverageItem klass, string methodName, string cov_info)
#endif
	{
		if (entry == null)
			// Compiler generated, abstract method etc.
			return;

		LineNumberEntry[] lines = entry.LineNumbers;

		// DEBUG
		//if( cov_info == null )
		//	Console.WriteLine( lines.Length );	
		// DEBUG

		if (lines.Length == 0)
			return;

		int start_line = lines [0].Row;
		int end_line = lines [lines.Length - 1].Row;

		MethodCoverageItem method 
			= new MethodCoverageItem (klass, methodName);

		method.startLine = start_line;
		method.endLine = end_line;
#if USE_REFLECTION
		method.filtered = IsFiltered ("[" + monoMethod.DeclaringType.Assembly + "]" + monoMethod.DeclaringType + "::" + monoMethod.Name);
#else
		method.filtered = IsFiltered ("[" + monoMethod.DeclaringType.Module.Name + "]" + monoMethod.DeclaringType + "::" + monoMethod.Name);
#endif
		klass.methodsByMethod [monoMethod] = method;


		if (klass.sourceFile == null) {
			string sourceFile = entry.SourceFile.FileName;

			SourceFileCoverageData source = (SourceFileCoverageData)sources [sourceFile];
			//Console.WriteLine( source );
			if (source == null) {
				source = new SourceFileCoverageData (sourceFile);
				sources [sourceFile] = source;
			}
			klass.sourceFile = source;
		}
			
		computeMethodCoverage (method, lines, cov_info);
	}
}
}
